import { render, screen, fireEvent } from '@testing-library/react';
import * as React from 'react';

import {
  SliderField,
  SLIDER_LABEL_TEST_ID,
  SLIDER_RANGE_TEST_ID,
  SLIDER_ROOT_TEST_ID,
  SLIDER_TRACK_TEST_ID,
} from '../SliderField';
import { Heading } from '../../Heading';
import { Fieldset } from '../../Fieldset';
import {
  testFlexProps,
  expectFlexContainerStyleProps,
} from '../../Flex/__tests__/Flex.test';
import { ComponentClassName } from '@aws-amplify/ui';
import { AUTO_GENERATED_ID_PREFIX } from '../../utils/useStableId';
import { ERROR_SUFFIX, DESCRIPTION_SUFFIX } from '../../../helpers/constants';
import { Button } from '../../Button';

// Jest uses JSDom, which apparently doesn't support the ResizeObserver API
// This will get around that API reference error in Jest
// We don't necessarily test its functionality since Radix should be responsible for that
window.ResizeObserver =
  window.ResizeObserver ||
  jest.fn().mockImplementation(() => ({
    disconnect: jest.fn(),
    observe: jest.fn(),
    unobserve: jest.fn(),
  }));

describe('SliderField:', () => {
  describe('Flex wrapper', () => {
    it('should render all flex style props', async () => {
      render(
        <SliderField
          defaultValue={0}
          testId="slider-field"
          label="slider"
          {...testFlexProps}
        />
      );
      const sliderField = await screen.findByTestId('slider-field');
      expectFlexContainerStyleProps(sliderField);
    });
  });

  describe('Label', () => {
    it('should render expected field classname', async () => {
      render(<SliderField defaultValue={0} label="slider" />);

      const label = await screen.findByTestId(SLIDER_LABEL_TEST_ID);
      expect(label).toHaveClass(
        ComponentClassName.Label,
        ComponentClassName.SliderFieldLabel
      );
    });

    it('should forward ref to DOM element', async () => {
      const ref = React.createRef<HTMLSpanElement>();
      const testId = 'sliderTestId';
      render(
        <SliderField
          defaultValue={0}
          label="slider"
          ref={ref}
          testId={testId}
        />
      );

      await screen.findByTestId(testId);
      expect(ref.current?.nodeName).toBe('SPAN');
    });

    it('should have `amplify-visually-hidden` class when labelHidden is true', async () => {
      render(<SliderField defaultValue={0} label="slider" labelHidden />);

      const label = await screen.findByTestId(SLIDER_LABEL_TEST_ID);
      expect(label).toHaveClass('amplify-visually-hidden');
    });

    it('should set autogenerated id', async () => {
      render(<SliderField label="slider" />);
      const label = await screen.findByTestId(SLIDER_LABEL_TEST_ID);
      expect(label.id.startsWith(AUTO_GENERATED_ID_PREFIX)).toBeTruthy();
    });

    it('should display value if isValueHidden is false', () => {
      render(<SliderField defaultValue={10} label="slider" />);
      const value = screen.queryByText('10');
      expect(value).toBeInTheDocument();
    });

    it('should display string formatted value if formatValue is provided', async () => {
      render(
        <SliderField
          defaultValue={10}
          label="slider"
          formatValue={(value) => {
            return `${value}%`;
          }}
        />
      );

      const value = await screen.findByText('10%');
      expect(value).toBeInTheDocument();
    });

    it('should display component-based formatted value if formatValue is provided', async () => {
      render(
        <SliderField
          defaultValue={10}
          label="slider"
          formatValue={(value) => {
            return <Heading>{value}</Heading>;
          }}
        />
      );

      const heading = await screen.findByText('10');
      expect(heading).toHaveClass(ComponentClassName.Heading);
    });

    it('should not display value if isValueHidden is true', () => {
      render(<SliderField defaultValue={10} label="slider" isValueHidden />);
      const value = screen.queryByText('10');
      expect(value).not.toBeInTheDocument();
    });
  });

  describe('Slider', () => {
    const ControlledSliderField = () => {
      const [value, setValue] = React.useState(5);
      return (
        <>
          <SliderField label="slider" value={value} onChange={setValue} />
          <Button testId="test-button" onClick={() => setValue(10)}>
            Set to 10
          </Button>
        </>
      );
    };

    it('should work as uncontrolled component', async () => {
      render(<SliderField defaultValue={10} label="slider" />);
      const slider = await screen.findByRole('slider');
      expect(slider).toHaveAttribute('aria-valuenow', '10');
      fireEvent.keyDown(slider, { key: 'ArrowUp', code: 'ArrowUp' });
      expect(slider).toHaveAttribute('aria-valuenow', '11');
      fireEvent.keyDown(slider, { key: 'ArrowUp', code: 'ArrowUp' });
      expect(slider).toHaveAttribute('aria-valuenow', '12');
      fireEvent.keyDown(slider, { key: 'ArrowDown', code: 'ArrowDown' });
      expect(slider).toHaveAttribute('aria-valuenow', '11');
      fireEvent.keyDown(slider, { key: 'ArrowDown', code: 'ArrowDown' });
      expect(slider).toHaveAttribute('aria-valuenow', '10');
    });

    it('should work as controlled component', async () => {
      render(<ControlledSliderField />);
      const slider = await screen.findByRole('slider');
      const label = await screen.findByTestId(SLIDER_LABEL_TEST_ID);
      expect(slider).toHaveAttribute('aria-valuenow', '5');
      expect(label).toHaveTextContent('5');
      fireEvent.keyDown(slider, { key: 'ArrowUp', code: 'ArrowUp' });
      expect(slider).toHaveAttribute('aria-valuenow', '6');
      expect(label).toHaveTextContent('6');
      fireEvent.keyDown(slider, { key: 'ArrowUp', code: 'ArrowUp' });
      expect(slider).toHaveAttribute('aria-valuenow', '7');
      expect(label).toHaveTextContent('7');
      fireEvent.keyDown(slider, { key: 'ArrowDown', code: 'ArrowDown' });
      expect(slider).toHaveAttribute('aria-valuenow', '6');
      expect(label).toHaveTextContent('6');
      fireEvent.keyDown(slider, { key: 'ArrowDown', code: 'ArrowDown' });
      expect(slider).toHaveAttribute('aria-valuenow', '5');
      expect(label).toHaveTextContent('5');

      // External update (remount) should work as well
      const button = await screen.findByTestId('test-button');
      fireEvent.click(button);
      expect(slider).toHaveAttribute('aria-valuenow', '10');
      expect(label).toHaveTextContent('10');
    });

    it('should set step', async () => {
      render(<SliderField defaultValue={0} label="slider" step={3} />);
      const slider = await screen.findByRole('slider');
      expect(slider).toHaveAttribute('aria-valuenow', '0');
      fireEvent.keyDown(slider, { key: 'ArrowUp', code: 'ArrowUp' });
      expect(slider).toHaveAttribute('aria-valuenow', '3');
      fireEvent.keyDown(slider, { key: 'ArrowUp', code: 'ArrowUp' });
      expect(slider).toHaveAttribute('aria-valuenow', '6');
      fireEvent.keyDown(slider, { key: 'ArrowDown', code: 'ArrowDown' });
      expect(slider).toHaveAttribute('aria-valuenow', '3');
      fireEvent.keyDown(slider, { key: 'ArrowDown', code: 'ArrowDown' });
      expect(slider).toHaveAttribute('aria-valuenow', '0');
    });

    it('should render size classes for SliderField', async () => {
      render(
        <div>
          <SliderField
            defaultValue={0}
            size="small"
            testId="small"
            label="slider"
            step={3}
          />
          <SliderField
            defaultValue={0}
            size="large"
            testId="large"
            label="slider"
            step={3}
          />
        </div>
      );

      const small = await screen.findByTestId('small');
      const large = await screen.findByTestId('large');

      expect(small.classList).toContain(
        `${ComponentClassName['Field']}--small`
      );
      expect(large.classList).toContain(
        `${ComponentClassName['Field']}--large`
      );
    });

    describe('Root', () => {
      it('should render default and custom classname', async () => {
        const className = 'class-test';
        render(
          <SliderField defaultValue={0} label="slider" className={className} />
        );
        const root = await screen.findByTestId(SLIDER_ROOT_TEST_ID);
        expect(root).toHaveClass(ComponentClassName.SliderFieldRoot, className);
      });

      it('should pass orientation prop', async () => {
        render(
          <SliderField defaultValue={0} label="slider" orientation="vertical" />
        );
        const root = await screen.findByTestId(SLIDER_ROOT_TEST_ID);
        expect(root).toHaveAttribute('data-orientation', 'vertical');
      });

      it('should be disabled', async () => {
        render(<SliderField defaultValue={0} label="slider" isDisabled />);
        const root = await screen.findByTestId(SLIDER_ROOT_TEST_ID);
        expect(root).toHaveAttribute('data-disabled', '');
      });

      it('should always be disabled if parent Fieldset isDisabled and SliderField isDisabled={false}', async () => {
        render(
          <Fieldset legend="legend" isDisabled>
            <SliderField defaultValue={0} label="slider" isDisabled={false} />
          </Fieldset>
        );

        const root = await screen.findByTestId(SLIDER_ROOT_TEST_ID);
        expect(root).toHaveAttribute('data-disabled', '');
      });
      it('should always be disabled if parent Fieldset isDisabled and SliderField isDisabled is not defined', async () => {
        render(
          <Fieldset legend="legend" isDisabled>
            <SliderField defaultValue={0} label="slider" />
          </Fieldset>
        );

        const root = await screen.findByTestId(SLIDER_ROOT_TEST_ID);
        expect(root).toHaveAttribute('data-disabled', '');
      });
    });

    describe('Track', () => {
      it('should render default classname', async () => {
        render(<SliderField defaultValue={0} label="slider" />);
        const track = await screen.findByTestId(SLIDER_TRACK_TEST_ID);
        expect(track).toHaveClass(ComponentClassName.SliderFieldTrack);
      });

      it('should set empty track color', async () => {
        render(
          <SliderField defaultValue={0} label="slider" emptyTrackColor="red" />
        );
        const track = await screen.findByTestId(SLIDER_TRACK_TEST_ID);
        expect(track).toHaveStyle('background-color: red');
      });

      it('should set track width for horizontal slider', async () => {
        render(<SliderField defaultValue={0} label="slider" trackSize="3px" />);
        const track = await screen.findByTestId(SLIDER_TRACK_TEST_ID);
        expect(track).toHaveStyle('height: 3px');
      });

      it('should set track width for vertical slider', async () => {
        render(
          <SliderField
            defaultValue={0}
            label="slider"
            orientation="vertical"
            trackSize="3px"
          />
        );
        const track = await screen.findByTestId(SLIDER_TRACK_TEST_ID);
        expect(track).toHaveStyle('width: 3px');
      });
    });

    describe('Range', () => {
      it('should render default classname', async () => {
        render(<SliderField defaultValue={0} label="slider" />);
        const range = await screen.findByTestId(SLIDER_RANGE_TEST_ID);
        expect(range).toHaveClass(ComponentClassName.SliderFieldRange);
      });

      it('should render orientation classes for SliderField', async () => {
        render(
          <SliderField defaultValue={0} label="slider" orientation="vertical" />
        );
        const vertical = await screen.findByTestId(SLIDER_RANGE_TEST_ID);
        expect(vertical).toHaveClass(
          `${ComponentClassName.SliderFieldRange}--vertical`
        );
      });

      it('should set filled track color', async () => {
        render(
          <SliderField
            defaultValue={0}
            label="slider"
            filledTrackColor="teal"
          />
        );
        const range = await screen.findByTestId(SLIDER_RANGE_TEST_ID);
        expect(range).toHaveStyle('background-color: teal');
      });
    });

    describe('Thumb', () => {
      it('should render default classname', async () => {
        render(<SliderField defaultValue={0} label="slider" />);
        const thumb = await screen.findByRole('slider');
        expect(thumb).toHaveClass(ComponentClassName.SliderFieldThumb);
      });

      it('should pass min and max props', async () => {
        render(
          <SliderField defaultValue={0} label="slider" min={-100} max={100} />
        );
        const thumb = await screen.findByRole('slider');
        expect(thumb).toHaveAttribute('aria-valuemin', '-100');
        expect(thumb).toHaveAttribute('aria-valuemax', '100');
      });

      it('should set thumb color', async () => {
        render(
          <SliderField defaultValue={0} label="slider" thumbColor="orange" />
        );
        const thumb = await screen.findByRole('slider');
        expect(thumb).toHaveStyle('background-color: orange');
      });

      it('should map to label correctly', async () => {
        render(
          <SliderField defaultValue={0} label="slider" id="slider-field" />
        );
        const thumb = await screen.findByRole('slider');
        expect(thumb).toHaveAccessibleName('slider 0');
      });

      it('should set aria-valuetext', async () => {
        render(
          <SliderField defaultValue={0} label="slider" ariaValuetext="Monday" />
        );
        const thumb = await screen.findByRole('slider');
        expect(thumb).toHaveAttribute('aria-valuetext', 'Monday');
      });
    });
  });

  describe('Error messages', () => {
    const errorMessage = 'This is an error message';
    it('should not show when hasError is false', () => {
      render(
        <SliderField
          defaultValue={0}
          label="slider"
          errorMessage={errorMessage}
        />
      );

      const errorText = screen.queryByText(errorMessage);
      expect(errorText).not.toBeInTheDocument();
    });

    it('show when hasError and errorMessage', () => {
      render(
        <SliderField
          defaultValue={0}
          label="slider"
          errorMessage={errorMessage}
          hasError
        />
      );
      const errorText = screen.queryByText(errorMessage);
      expect(errorText?.innerHTML).toContain(errorMessage);
    });
  });

  describe('descriptive message', () => {
    it('should render descriptiveText if it is provided', () => {
      render(
        <SliderField
          defaultValue={0}
          label="slider"
          descriptiveText="Description"
        />
      );

      const descriptiveText = screen.queryByText('Description');
      expect(descriptiveText?.innerHTML).toContain('Description');
    });

    it('should map to descriptive text correctly', async () => {
      render(
        <SliderField
          defaultValue={0}
          label="slider"
          descriptiveText="Description"
        />
      );

      const slider = await screen.findByRole('slider');
      expect(slider).toHaveAccessibleDescription('Description');
    });
  });

  describe('aria-describedby test', () => {
    const errorMessage = 'This is an error message';
    const descriptiveText = 'Description';
    it('when hasError, include id of error component and describe component in the aria-describedby', async () => {
      render(
        <SliderField
          defaultValue={0}
          label="slider"
          descriptiveText={descriptiveText}
          errorMessage={errorMessage}
          hasError
        />
      );

      const thumb = await screen.findByRole('slider');
      const ariaDescribedBy = thumb.getAttribute('aria-describedby');
      const descriptiveTextElement = screen.queryByText(descriptiveText);
      const errorTextElement = screen.queryByText(errorMessage);
      expect(
        errorTextElement?.id && errorTextElement?.id.endsWith(ERROR_SUFFIX)
      ).toBe(true);
      expect(
        descriptiveTextElement?.id &&
          descriptiveTextElement?.id.endsWith(DESCRIPTION_SUFFIX)
      ).toBe(true);
      expect(
        errorTextElement?.id &&
          descriptiveTextElement?.id &&
          ariaDescribedBy ===
            `${errorTextElement.id} ${descriptiveTextElement.id}`
      ).toBe(true);
    });

    it('only show id of describe component in aria-describedby when hasError is false', async () => {
      render(
        <SliderField
          defaultValue={0}
          label="slider"
          descriptiveText={descriptiveText}
          errorMessage={errorMessage}
        />
      );

      const thumb = await screen.findByRole('slider');
      const ariaDescribedBy = thumb.getAttribute('aria-describedby');
      const descriptiveTextElement = screen.queryByText(descriptiveText);
      expect(
        descriptiveTextElement?.id &&
          ariaDescribedBy?.startsWith(descriptiveTextElement?.id)
      ).toBe(true);
    });

    it('aria-describedby should be empty when hasError is false and descriptiveText is empty', async () => {
      render(
        <SliderField
          defaultValue={0}
          label="slider"
          errorMessage={errorMessage}
        />
      );

      const thumb = await screen.findByRole('slider');
      const ariaDescribedBy = thumb.getAttribute('aria-describedby');
      expect(ariaDescribedBy).toBeNull();
    });
  });
});
